|
1
|
|
|
/** |
|
2
|
|
|
* The main fisherman class, to create commands and interact with them |
|
3
|
|
|
* @author Maxerbox | Simon Sassi 2017 |
|
4
|
|
|
* @extends {EventEmitter} |
|
5
|
|
|
* @class Fisherman |
|
6
|
|
|
*/ |
|
7
|
|
|
const async = require('async') |
|
8
|
|
|
const FisherRegister = require('./register.js') |
|
9
|
|
|
const defaultFisherOpts = require('./util/FishermanOptions') |
|
10
|
|
|
const escapeRegExp = require('./util/RegExpEscape') |
|
11
|
|
|
var fisherRouter = require('./router/router') |
|
12
|
|
|
const EventEmitter = require('events') |
|
13
|
|
|
const fisherCodes = require('./util/FisherCodes') |
|
14
|
|
|
const CommandNotFoundException = require('./exceptions/CommandNotFoundException') |
|
15
|
|
|
const InvalidChannelException = require('./exceptions/InvalidChannelException') |
|
16
|
|
|
const InvalidPatternException = require('./exceptions/InvalidPatternException') |
|
17
|
|
|
const MissingPermissionsException = require('./exceptions/MissingPermissionsException') |
|
18
|
|
|
|
|
19
|
|
|
class Fisherman extends EventEmitter { |
|
20
|
|
|
/** |
|
21
|
|
|
* Creates an instance of Fisherman. |
|
22
|
|
|
* @param {FishermanOptions} options The options for fisherman |
|
23
|
|
|
* @memberof Fisherman |
|
24
|
|
|
*/ |
|
25
|
|
|
constructor (options = {}) { |
|
26
|
|
|
super() |
|
27
|
|
|
/** |
|
28
|
|
|
* Used to instantiate the FisherRouter |
|
29
|
|
|
* @name Fisherman#fisherRouterPrototype |
|
30
|
|
|
* @type {FisherRouter} |
|
31
|
|
|
*/ |
|
32
|
|
|
this.fisherRouterPrototype = fisherRouter |
|
33
|
|
|
/** |
|
34
|
|
|
* The middleware handling function stack |
|
35
|
|
|
* @private |
|
36
|
|
|
* @name Fisherman#handleListeners |
|
37
|
|
|
* @type {Array} |
|
38
|
|
|
*/ |
|
39
|
|
|
this.handleListeners = [] |
|
40
|
|
|
/** |
|
41
|
|
|
* The middleware function stack on init |
|
42
|
|
|
* @private |
|
43
|
|
|
* @name Fisherman#messageListeners |
|
44
|
|
|
* @type {Array} |
|
45
|
|
|
*/ |
|
46
|
|
|
this.setUpListeners = [] |
|
47
|
|
|
/** |
|
48
|
|
|
* All the commands handled by fisherman |
|
49
|
|
|
* @name Fisherman#commands |
|
50
|
|
|
* @type {Map.<string, Command>} |
|
51
|
|
|
*/ |
|
52
|
|
|
this.commands = new Map() |
|
53
|
|
|
/** |
|
54
|
|
|
* All the command aliases handled by fisherman |
|
55
|
|
|
* @name Fisherman#aliases |
|
56
|
|
|
* @type {Map.<string, Command>} |
|
57
|
|
|
*/ |
|
58
|
|
|
this.aliases = new Map() |
|
59
|
|
|
/** |
|
60
|
|
|
* All the command registers handled by fisherman |
|
61
|
|
|
* @name Fisherman#registers |
|
62
|
|
|
* @type {Map.<string, FisherRegister>} |
|
63
|
|
|
*/ |
|
64
|
|
|
this.registers = new Map() |
|
65
|
|
|
/** |
|
66
|
|
|
* A fastfall empty callback |
|
67
|
|
|
* @private |
|
68
|
|
|
* @name Fisherman#fallHandle |
|
69
|
|
|
*/ |
|
70
|
|
|
this.fallHandle = require('fastfall')(this.handleListeners) |
|
71
|
|
|
this.setOptions(options) |
|
72
|
|
|
if (!this.client) { this.client = new (require('discord.js')).Client(this.clientOptions) } |
|
73
|
|
|
} |
|
74
|
|
|
/** |
|
75
|
|
|
* Set the options to fisherman |
|
76
|
|
|
* |
|
77
|
|
|
* @param {FishermanOptions} options |
|
78
|
|
|
* @memberof Fisherman |
|
79
|
|
|
*/ |
|
80
|
|
|
setOptions (options) { |
|
81
|
|
|
var opts = Object.assign(defaultFisherOpts, options) |
|
82
|
|
|
if (opts.client) { this.client = opts.client } |
|
83
|
|
|
if (opts.prefixes) { this.setPrefixe(opts.prefixes) } |
|
84
|
|
|
this.commandMatchRegExp = new RegExp(opts.commandMatchRegExp) |
|
85
|
|
|
this.ownerID = opts.ownerID |
|
86
|
|
|
this.clientOptions = options.clientOptions |
|
87
|
|
|
this.sendAliasStatus = opts.sendAliasStatus |
|
88
|
|
|
this.sendNotFoundStatus = opts.sendNotFoundStatus |
|
89
|
|
|
this.selfMessageProcessing = opts.selfMessageProcessing |
|
90
|
|
|
} |
|
91
|
|
|
|
|
92
|
|
|
/** |
|
93
|
|
|
* |
|
94
|
|
|
* Set the prefixe |
|
95
|
|
|
* @param {(Array|string)} prefixes |
|
96
|
|
|
* @memberof Fisherman |
|
97
|
|
|
*/ |
|
98
|
|
|
setPrefixe (prefixes) { |
|
99
|
|
|
if (typeof prefixes === 'string') { |
|
100
|
|
|
this.regPref = new RegExp('^(' + escapeRegExp.escapeString(prefixes) + ').*') |
|
101
|
|
|
} else if (Array.isArray(prefixes)) { |
|
102
|
|
|
var escapedArray = escapeRegExp.escapeArray(prefixes) |
|
103
|
|
|
this.regPref = new RegExp('^(' + escapedArray.join('|') + ').*') |
|
104
|
|
|
} |
|
105
|
|
|
} |
|
106
|
|
|
/** |
|
107
|
|
|
* Create a Fisherman instance from an already logged in discord.js client |
|
108
|
|
|
* |
|
109
|
|
|
* @static |
|
110
|
|
|
* @param {Client} client The discord.js client |
|
111
|
|
|
* @param {FishermanOptions} fisherOptions The options for Fisherman |
|
112
|
|
|
* @memberof Fisherman |
|
113
|
|
|
*/ |
|
114
|
|
|
static createFromClient (client, fisherOptions = {}) { |
|
115
|
|
|
var opts = Object.assign({ client: client }, fisherOptions) |
|
116
|
|
|
return new this(opts) |
|
117
|
|
|
} |
|
118
|
|
|
|
|
119
|
|
|
/** |
|
120
|
|
|
* |
|
121
|
|
|
* Message event listener |
|
122
|
|
|
* @private |
|
123
|
|
|
* @param {Message} message A discord.js Message |
|
124
|
|
|
* @memberof Fisherman |
|
125
|
|
|
*/ |
|
126
|
|
|
handleMessage (message) { |
|
127
|
|
|
if (message.author.id === this.client.user.id && !this.selfMessageProcessing) { return } |
|
128
|
|
|
var router = fisherRouter.buildFromMessage(this, message) |
|
129
|
|
|
var that = this |
|
130
|
|
|
var prefixe = router.request.prefix = this.checkPrefixe(message.content) |
|
131
|
|
|
try { |
|
132
|
|
|
router.request.command = prefixe ? this.checkCommand(prefixe, message) : null |
|
133
|
|
|
if (router.request.command) router.request.isCommand = true |
|
|
|
|
|
|
134
|
|
|
} catch (err) { |
|
135
|
|
|
router.response.sendCode(err.code, err) |
|
136
|
|
|
return |
|
137
|
|
|
} |
|
138
|
|
|
this.fallHandle(router.request, router.response, function (err, request, response) { |
|
139
|
|
|
if (err) return err === true ? undefined : router.response.sendCode(fisherCodes.MIDDLEWARE_FAILED, err) |
|
140
|
|
|
if (router.request.command) { |
|
141
|
|
|
var cmd = router.request.command |
|
142
|
|
|
that.matchSuffixe(cmd, cmd.suffixe, function (result) { |
|
143
|
|
|
if (result) { |
|
144
|
|
|
if (cmd.isPromise) { |
|
145
|
|
|
(new Promise(function (resolve, reject) { |
|
146
|
|
|
cmd.execute(router.request, router.response, resolve, reject) |
|
147
|
|
|
})).then(res => router.response.sendCode(fisherCodes.COMMAND_SUCESS, res)).catch(err => router.response.sendCode(fisherCodes.COMMAND_FAILED, err)) |
|
148
|
|
|
} else { |
|
149
|
|
|
cmd.execute(router.request, router.response) |
|
150
|
|
|
} |
|
151
|
|
|
} else { |
|
152
|
|
|
router.response.sendCode(fisherCodes.INVALID_PATTERN, new InvalidPatternException(cmd.suffixe)) |
|
153
|
|
|
} |
|
154
|
|
|
}) |
|
|
|
|
|
|
155
|
|
|
} |
|
156
|
|
|
}) |
|
157
|
|
|
} |
|
158
|
|
|
|
|
159
|
|
|
/** |
|
160
|
|
|
* Check if there is a command, throw exceptions |
|
161
|
|
|
* @private |
|
162
|
|
|
* @param {string} prefixe The command prefixe |
|
163
|
|
|
* @param {Message} message A discord.js message |
|
164
|
|
|
* @returns {Array} |
|
165
|
|
|
* @memberof Fisherman |
|
166
|
|
|
*/ |
|
167
|
|
|
checkCommand (prefixe, message) { |
|
168
|
|
|
/* |
|
169
|
|
|
Benchmark split vs regex match : https://jsperf.com/regex-vs-split/2 |
|
170
|
|
|
Benchmark inline RegExp vs Stored RegExp : https://jsperf.com/regexp-indexof-perf/24 |
|
171
|
|
|
*/ |
|
172
|
|
|
var textCmd = message.content.substring(prefixe.length).match(this.commandMatchRegExp)[0] |
|
173
|
|
|
var cmd = this.commands.get(textCmd) || this.aliases.get(textCmd) |
|
174
|
|
|
cmd = this.validateCommand(message, cmd, textCmd) ? cmd : null |
|
175
|
|
|
if (cmd) cmd.suffixe = textCmd ? message.content.substring(prefixe.length + textCmd.length + 1) : '' |
|
|
|
|
|
|
176
|
|
|
return cmd |
|
177
|
|
|
} |
|
178
|
|
|
/** |
|
179
|
|
|
* |
|
180
|
|
|
* @private |
|
181
|
|
|
* @param {command} cmd |
|
182
|
|
|
* @param {string} suffixe |
|
183
|
|
|
* @param {function} validate |
|
184
|
|
|
* @memberof Fisherman |
|
185
|
|
|
*/ |
|
186
|
|
|
matchSuffixe (cmd, suffixe, validate) { |
|
187
|
|
|
if (cmd.regPattern) { |
|
188
|
|
|
validate(cmd.regPattern.test(suffixe)) |
|
189
|
|
|
} else if (cmd.patternCallback) { |
|
190
|
|
|
cmd.patternCallback.test(suffixe, validate) |
|
191
|
|
|
} else { |
|
192
|
|
|
validate(true) |
|
193
|
|
|
} |
|
194
|
|
|
} |
|
195
|
|
|
/** |
|
196
|
|
|
* Validate a command, throw exceptions |
|
197
|
|
|
* @private |
|
198
|
|
|
* @param {Message} message A discord.js message |
|
199
|
|
|
* @param {command} cmd the command |
|
200
|
|
|
* @returns {boolean} |
|
201
|
|
|
* @memberof Fisherman |
|
202
|
|
|
*/ |
|
203
|
|
|
validateCommand (message, cmd, textCmd) { |
|
204
|
|
|
if (!cmd && this.sendNotFoundStatus) { throw new CommandNotFoundException(textCmd) } else if (!cmd) { return false } |
|
205
|
|
|
if (cmd.channelType.indexOf(message.channel.type) === -1) { throw new InvalidChannelException(message.channel.type) } |
|
206
|
|
|
let permissions = message.guild ? message.channel.permissionsFor(this.client.user) : null |
|
207
|
|
|
if (permissions && permissions.has(cmd.discordPermRequired)) { |
|
208
|
|
|
let missing = permissions.missing(cmd.discordSpecialPerms) |
|
209
|
|
|
if (missing.length > 0) { throw new MissingPermissionsException(missing) } |
|
210
|
|
|
} |
|
211
|
|
|
return true |
|
212
|
|
|
} |
|
213
|
|
|
/** |
|
214
|
|
|
* Check if there is a prefixe in content |
|
215
|
|
|
* @private |
|
216
|
|
|
* @param {string} content |
|
217
|
|
|
* @returns {string} |
|
218
|
|
|
* @memberof Fisherman |
|
219
|
|
|
*/ |
|
220
|
|
|
checkPrefixe (content) { |
|
221
|
|
|
var result = this.regPref.exec(content) |
|
222
|
|
|
// return (Array.isArray(result)) ? result[1] : null; |
|
223
|
|
|
return result ? result[1] : null |
|
224
|
|
|
} |
|
225
|
|
|
/** |
|
226
|
|
|
* Initialize Fisherman and the middlewares |
|
227
|
|
|
* @param {string} [token = null] The token to log in with, optional if the client is already connected |
|
|
|
|
|
|
228
|
|
|
* @param {function} callback An optional callback to trigger when Fisherman is initialized |
|
229
|
|
|
* @memberof Fisherman |
|
230
|
|
|
* @fires Fisherman#initialized |
|
231
|
|
|
*/ |
|
232
|
|
|
init (token = null, callback) { |
|
233
|
|
|
/** |
|
234
|
|
|
* Emitted when Fisherman and middlewares are initialized |
|
235
|
|
|
* @event Fisherman#initialized |
|
236
|
|
|
*/ |
|
237
|
|
|
this.client.on('message', this.handleMessage.bind(this)) |
|
238
|
|
|
var that = this |
|
239
|
|
|
if (!token) { |
|
240
|
|
|
this.initializeMiddleware(callback) |
|
241
|
|
|
} else { |
|
242
|
|
|
this.client.login(token).then(() => { |
|
243
|
|
|
that.initializeMiddleware(callback) |
|
244
|
|
|
}) |
|
245
|
|
|
} |
|
246
|
|
|
} |
|
247
|
|
|
/** |
|
248
|
|
|
* Initialize the middlewares |
|
249
|
|
|
* @private |
|
250
|
|
|
* |
|
251
|
|
|
* @memberof Fisherman |
|
252
|
|
|
*/ |
|
253
|
|
|
initializeMiddleware (callback) { |
|
254
|
|
|
var that = this |
|
255
|
|
|
async.parallel(this.setUpListeners, function (err) { |
|
256
|
|
|
if (err) { throw err } |
|
257
|
|
|
if (typeof callback === 'function') { callback() } |
|
258
|
|
|
that.emit('initialized') |
|
259
|
|
|
}) |
|
260
|
|
|
} |
|
261
|
|
|
|
|
262
|
|
|
/** |
|
263
|
|
|
* Create a new register to add commands |
|
264
|
|
|
* @fires Fisherman#registerAdded |
|
265
|
|
|
* @param {string} keyName The register key value, to set in the registers map |
|
266
|
|
|
* @param {string} [registerName = null] The register's name |
|
|
|
|
|
|
267
|
|
|
* @param {string} [registerDescription = null] The register's description |
|
|
|
|
|
|
268
|
|
|
* @return {FisherRegister} Return a FisherRegister instance |
|
269
|
|
|
* @memberof Fisherman |
|
270
|
|
|
*/ |
|
271
|
|
|
createRegister (keyName, registerName = null, registerDescription = null) { |
|
272
|
|
|
/** |
|
273
|
|
|
* Emitted when a new register is added |
|
274
|
|
|
* @event Fisherman#registerAdded |
|
275
|
|
|
*/ |
|
276
|
|
|
var register = new FisherRegister(this, registerName || keyName, registerDescription) |
|
277
|
|
|
this.registers.set(keyName, register) |
|
278
|
|
|
return register |
|
279
|
|
|
} |
|
280
|
|
|
/** |
|
281
|
|
|
* |
|
282
|
|
|
* Add a middleware to Fisherman |
|
283
|
|
|
* @param {(function|Object)} middleware The middleware function|class |
|
284
|
|
|
* @return {Fisherman} |
|
285
|
|
|
* @memberof Fisherman |
|
286
|
|
|
*/ |
|
287
|
|
|
use (middleware) { |
|
288
|
|
|
if (typeof middleware !== 'object' && typeof middleware !== 'function') throw new TypeError('A middleware must be a function or an object') |
|
|
|
|
|
|
289
|
|
|
this.appendMiddleware(middleware) |
|
290
|
|
|
return this |
|
291
|
|
|
} |
|
292
|
|
|
|
|
293
|
|
|
/** |
|
294
|
|
|
* |
|
295
|
|
|
* Add a middleware to Fisherman |
|
296
|
|
|
* @private |
|
297
|
|
|
* @param {function} middleware The middleware function|class |
|
298
|
|
|
* @memberof Fisherman |
|
299
|
|
|
*/ |
|
300
|
|
|
appendMiddleware (middleware) { |
|
301
|
|
|
if (typeof middleware.setUp === 'function') { this.setUpListeners.push(middleware.setUp.bind(middleware, this)) } |
|
302
|
|
|
if (typeof middleware.handle === 'function') { |
|
303
|
|
|
this.handleListeners.push(middleware.handle.bind(middleware)) |
|
304
|
|
|
this.fallHandle = require('fastfall')(this.handleListeners) |
|
305
|
|
|
} |
|
306
|
|
|
} |
|
307
|
|
|
} |
|
308
|
|
|
module.exports = Fisherman |
|
309
|
|
|
|
Consider adding curly braces around all statements when they are executed conditionally. This is optional if there is only one statement, but leaving them out can lead to unexpected behaviour if another statement is added later.
Consider:
If you or someone else later decides to put another statement in, only the first statement will be executed.
In this case the statement
b = 42will always be executed, while the logging statement will be executed conditionally.ensures that the proper code will be executed conditionally no matter how many statements are added or removed.